using System;
using System.Collections.Generic;
using System.Linq;

using EMan.Common.EnterpriseStructure;

using TrueCode.Core;
using TrueCode.Core.Aspects;
using TrueCode.Core.EditableEntity;

namespace EMan.BL.EnterpriseStructure
{
  internal static class StructureChangsetHelper
  {
    public static StructureChangeset MakeChangeset( IEnumerable< DepartmentNode > treeRoots )
    {
      var builder = new ChangesetBuilder( treeRoots );
      return builder.Build();
    }

    public static void MergeChangeset( StructureChangeset changeset, ICollection< DepartmentNode > treeRoots )
    {
      var deletedNodes = new List< DepartmentNode >();
      foreach( var deletedDepartmentId in changeset.DeletedDepartments )
        DFS( treeRoots, node => node.Current.Id == deletedDepartmentId, deletedNodes.Add );

      foreach( var deletedNode in deletedNodes )
      {
        if( deletedNode.ParentNode == null )
          treeRoots.Remove( deletedNode );
        else
          deletedNode.ParentNode.Remove( deletedNode );
      }

      foreach( var tuple in changeset.NewDepartments )
        DFS( treeRoots,
          node => node.Current.Key == tuple.ItemB.Key,
          node => MergeTreeOfNewDepartments( node, tuple.ItemB ) );

      foreach( var department in changeset.ModifiedDepartments )
        DFS( treeRoots, 
          node => node.Current.Id == department.Id, 
          node => {
                 node.Current = department;
                 node.AcceptChanges(); 
        } );
    }

    private static void DFS( IEnumerable< DepartmentNode > roots, Predicate< DepartmentNode > predicate, Action< DepartmentNode > action )
    {
      foreach( var node in roots )
        DFS( node, predicate, action );
    }

    private static void DFS( DepartmentNode root, Predicate< DepartmentNode > predicate, Action< DepartmentNode > action )
    {
      if( predicate( root ) )
        action( root );
      else
        DFS( root.DepartmentNodes, predicate, action );
    }

    private static void MergeTreeOfNewDepartments( DepartmentNode node, Department department )
      {
        foreach( var subDepartment in department.Departments )
          MergeTreeOfNewDepartments( node.DepartmentNodes.Single( n => n.Key == subDepartment.Key ), subDepartment );

        node.Current = department;
        node.AcceptChanges();
      }

    #region class ChangesetBuilder
    /// <summary>
    /// It is a helper class used in order to do not pass a lot of parameters in method of handling of tree
    /// </summary>
    private class ChangesetBuilder
    {
      #region Fields
      private readonly IEnumerable< DepartmentNode > _treeRoots;

      private readonly List< Tuple< Guid, Department > > _newDepartments = new List< Tuple< Guid, Department > >();
      private readonly List< Department > _modifiedDepartments = new List< Department >();
      private readonly List< int > _deletedDepartments = new List< int >();

      private readonly List< Tuple< Guid, Employee > > _newEmployees = new List< Tuple< Guid, Employee > >();
      private readonly List< Employee > _modifiedEmployees = new List< Employee >();
      private readonly List< int > _deletedEmployees = new List< int >();
      #endregion

      #region .ctor
      public ChangesetBuilder( [NotEmpty] IEnumerable< DepartmentNode > treeRoots )
      {
        if( treeRoots == null || !treeRoots.Any() ) throw new ArgumentNullException( "treeRoots" );
        _treeRoots = treeRoots;
      }
      #endregion

      #region Entry points
      public StructureChangeset Build()
      {
        foreach( var root in _treeRoots )
          ProcessTreeBranch( root );
        return new StructureChangeset( _newDepartments, _modifiedDepartments, _deletedDepartments, _newEmployees, _modifiedEmployees, _deletedEmployees );
      }
      #endregion

      #region Private
      private void ProcessTreeBranch( DepartmentNode branchRoot )
      {
        // If a department is deleted, add it and all sub departments and all employees into collections of ids of deleted entities
        // and do not process any children
        if( branchRoot.State == EditableObjectState.Deleted )
          AddDepartmentTreeToDeleted( branchRoot );
        else if( branchRoot.State == EditableObjectState.New )
        {
          // If root of the branch is new, all its children (if presented) are new, so add a root with all children, and do not process children
          var copy = branchRoot.Current.WholeTreeCopy();
          _newDepartments.Add( new Tuple< Guid, Department >( branchRoot.Key, copy ) );
        }
        else
        {
          // If department is dirty add it to the list without children
          if( branchRoot.State == EditableObjectState.Modified )
            _modifiedDepartments.Add( branchRoot.Current.Copy() );

          // Process all employees
          foreach( var employeeNode in branchRoot.EmployeeNodes )
          {
            var employee = employeeNode.Current;
            switch( employeeNode.State )
            {
              case EditableObjectState.New:
                _newEmployees.Add( new Tuple< Guid, Employee >( employeeNode.Key, employee ) );
                break;
              case EditableObjectState.Modified:
                _modifiedEmployees.Add( employeeNode.Current );
                break;
              case EditableObjectState.Deleted:
                _deletedEmployees.Add( employee.Id );
                break;
              case EditableObjectState.Original:
                break;
              default:
                throw new ArgumentOutOfRangeException();
            }
          }

          // Recursively process all subdepartments
          foreach( var subNode in branchRoot.DepartmentNodes )
            ProcessTreeBranch( subNode );
        }
      }

      /// <summary>
      /// Adds <paramref name="departmentNode"/> and whole retained tree into list of deleted departments recursively.
      /// </summary>
      /// <param name="departmentNode">Node - the root of the tree.</param>
      private void AddDepartmentTreeToDeleted( DepartmentNode departmentNode )
      {
        // add sub departments first - they will be deleted first and FK violation is avoided
        foreach( var subNode in departmentNode.DepartmentNodes )
          AddDepartmentTreeToDeleted( subNode );

        foreach( var employeeNode in departmentNode.EmployeeNodes )
          _deletedEmployees.Add( employeeNode.Current.Id );

        _deletedDepartments.Add( departmentNode.Current.Id );
      }
      #endregion
    }
    #endregion

  }
}